perf(memtrack): reduce serialization bottleneck#436
Conversation
Merging this PR will improve performance by 17.3%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | write_events[1000000] |
2.1 s | 1.8 s | +18.23% |
| ⚡ | Simulation | write_events[10000] |
20 ms | 17 ms | +17.65% |
| ⚡ | Simulation | write_events[100000] |
207.3 ms | 177.6 ms | +16.72% |
| ⚡ | Simulation | write_events[500000] |
1,036.6 ms | 889 ms | +16.61% |
| 🆕 | Simulation | encode_pipeline[100000] |
N/A | 175.6 ms | N/A |
| 🆕 | Simulation | encode_pipeline[1000000] |
N/A | 1.8 s | N/A |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing cod-3071-fix-serialization-bottleneck (92a19a6) with main (7ce2c98)
381bac8 to
9be9b22
Compare
Greptile SummaryThis PR significantly restructures the memtrack encoding pipeline for performance: the old single-writer-thread design (one event per channel message, BufWriter in the wrong position relative to zstd) is replaced with a batched parallel pipeline that serializes and compresses batches concurrently on
Confidence Score: 4/5Safe to merge for production use; the integration test helper has a bug that will cause all tests tracking processes with fewer than 64K allocation events to collect zero events. The production pipeline in main.rs correctly calls stop_polling() before joining the encode thread, so real runs are unaffected. However, tests/shared.rs never calls stop_polling() — the collection loop exits on a 10-second recv_timeout rather than on channel close, so the final partial batch (which IS the only batch for any process generating fewer than 64 K events) is flushed into an unread channel after the loop has already exited. Integration tests relying on this helper will silently capture zero events. crates/memtrack/tests/shared.rs — track_command needs to call tracker.stop_polling() after the child exits and then drain rx to channel close rather than using recv_timeout as the exit signal. Important Files Changed
|
The custom Serialize impl emits ~13 tiny writes per event (map header + per-field key/value). With the BufWriter on the compressed output side, each of those hit zstd's streaming compressor directly, whose per-call overhead dominated: ~2.4x slower than the derived flatten path, which coalesced each event into one write. Move the BufWriter to the encoder's input side so the tiny writes are batched before reaching zstd. Restores baseline throughput.
The drain path fed events through three per-event std::mpsc hops (poll -> keepalive -> drain -> dispatcher) before batching, each send allocating a node and locking. Batch events in the poll callback instead: the single poll thread coalesces into fixed-size batches and hands off one Vec at a time. The poller now lives in the Tracker (removing the keepalive thread) and track() returns batches; main's drain thread is gone and the dispatcher just tags ordered batches with a sequence number. Per-event work drops from three mpsc sends to one Vec push.
The drain->encode->write pipeline lived inline in main.rs, so it was reachable only through the eBPF path and had no test or benchmark coverage. Move it into runner_shared::artifacts::memtrack::encode_batches, which takes any ordered batch source (the live tracker channel or synthetic data) and an output writer. Split the memtrack artifact module into mod/writer/pipeline. main.rs now spawns one thread running encode_batches. Adds unit tests for ordering across parallel workers and the empty-run frame, plus an encode_pipeline divan benchmark writing to a sink.
Summary
MemtrackEventserialization with a byte-identical manual serializerMemtrackWriter::finishreturn encoded frame bytes for batchingVerification
cargo test -p runner-shared artifacts::memtrackcargo test -p runner-sharedcargo check -p runner-sharedcargo test --manifest-path crates/memtrack/Cargo.tomlvia Nix shell with libclang/build deps; eBPF integration cases compiled but were ignored becauseGITHUB_ACTIONSwas unsetcargo bench -p runner-shared --bench memtrack_writerNotes
memtrack_writerbench measures the Phase A single-writer encoder path only, not the full parallel memtrack pipeline.